/* CsJni - .NET / Java Bridge
 * 
 * Copyright 2010 Robert E. Burke Licensed under the Apache License, Version 
 * 2.0 (the "License"); you may not use this file except in compliance with 
 * the License. You may obtain a copy of the License at 
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable 
 * law or agreed to in writing, software distributed under the License is 
 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 
 * KIND, either express or implied. See the License for the specific language 
 * governing permissions and limitations under the License. 
*/

using System;
using System.Collections.Generic;
using System.Text;
using System.Diagnostics.CodeAnalysis;

namespace CsJni
{
    public enum JniObjectLifecycle
    {
        Dead,
        Local,
        LocalJ2N,
        Global,
        GlobalAutoRelease,
    }

    public enum JniType
    {
        Null,
        Void, // Only for method return types
        Boolean,
        Byte,
        Char,
        Double,
        Float,
        Int,
        Long,
        Short,
        String, // treat string specially
        Class,
        Throwable,
        NonPrimitive,
    }

    public enum JniConcreteObjectType
    {
        None,
        Object,
        String,
        Class,
        Throwable,
        BooleanArray,
        ByteArray,
        CharArray,
        DoubleArray,
        IntArray,
        FloatArray,
        LongArray,
        ShortArray,
        ObjectArray,
    }

    public class JniObject
    {
        private IntPtr mHandle;
        private JniObjectLifecycle mLifeCycle;
        private bool mWillFinalize = true;

        protected internal JniObject(JniEnvironment environment, IntPtr handle, JniObjectLifecycle lifecycle)
        {
            if (handle == IntPtr.Zero)
            {
                throw new JniException("TODO");
            }
            if (lifecycle == JniObjectLifecycle.Dead)
            {
                throw new JniException("Invalid starting state");
            }
            mHandle = handle;
            mLifeCycle = lifecycle;
            if (mLifeCycle == JniObjectLifecycle.Local)
            {
                environment.AddToCurrentFrame(this);
            }
            else if (mLifeCycle == JniObjectLifecycle.LocalJ2N)
            {
                mWillFinalize = false;
                GC.SuppressFinalize(this);
            }
        }

        ~JniObject()
        {
            bool triedToRelease = false;
            if (mLifeCycle == JniObjectLifecycle.GlobalAutoRelease)
            {
                JniEnvironment environment = JavaVM.SafeFindEnvironment();
                if (environment != null)
                {
                    this.Release(environment);
                }
                else
                {
                    triedToRelease = true;
                }
            }
            if (!this.IsDead)
            {
                JavaVM.LogMemory(triedToRelease, "Leaked Java memory: " + mHandle.ToString() + ", " + 
                    mLifeCycle.ToString() + ", " + ConcreteType.ToString());
            }
        }

        public virtual JniConcreteObjectType ConcreteType
        {
            get { return JniConcreteObjectType.Object; }
        }

        public static JniObject CreateObjectOfTypeFromJ2N(JniEnvironment environment, IntPtr handle, 
            JniConcreteObjectType concreteType)
        {
            return CreateObjectOfType(environment, handle, concreteType, JniObjectLifecycle.LocalJ2N);
        }

        internal static JniObject CreateObjectOfType(JniEnvironment environment, IntPtr handle,
            JniObject originalObject)
        {
            return CreateObjectOfType(environment, handle, originalObject, JniObjectLifecycle.Local);
        }

        internal static JniObject CreateObjectOfType(JniEnvironment environment, IntPtr handle, 
            JniConcreteObjectType concreteType)
        {
            return CreateObjectOfType(environment, handle, concreteType, JniObjectLifecycle.Local);
        }

        private static JniObject CreateObjectOfType(JniEnvironment environment, IntPtr handle,
            JniObject originalObject, JniObjectLifecycle lifecycle)
        {
            if (originalObject.ConcreteType == JniConcreteObjectType.ObjectArray)
            {
                JniObjectArray objectArray = originalObject as JniObjectArray;
                return new JniObjectArray(environment, handle, lifecycle,
                    objectArray.ArrayRank,
                    objectArray.ElementType);
            }
            return CreateObjectOfType(environment, handle, originalObject.ConcreteType, lifecycle);
        }

        private static JniObject CreateObjectOfType(JniEnvironment environment, IntPtr handle, 
            JniConcreteObjectType concreteType, JniObjectLifecycle lifecycle)
        {
            switch (concreteType)
            {
                case JniConcreteObjectType.Object: return new JniObject(environment, handle, lifecycle);
                case JniConcreteObjectType.String: return new JniString(environment, handle, lifecycle);
                case JniConcreteObjectType.Class: return new JniClass(environment, handle, lifecycle);
                case JniConcreteObjectType.Throwable: return new JniThrowable(environment, handle, lifecycle);
                case JniConcreteObjectType.BooleanArray: return new JniBooleanArray(environment, handle, lifecycle);
                case JniConcreteObjectType.ByteArray: return new JniByteArray(environment, handle, lifecycle);
                case JniConcreteObjectType.CharArray: return new JniCharArray(environment, handle, lifecycle);
                case JniConcreteObjectType.DoubleArray: return new JniDoubleArray(environment, handle, lifecycle);
                case JniConcreteObjectType.IntArray: return new JniIntArray(environment, handle, lifecycle);
                case JniConcreteObjectType.FloatArray: return new JniFloatArray(environment, handle, lifecycle);
                case JniConcreteObjectType.LongArray: return new JniLongArray(environment, handle, lifecycle);
                case JniConcreteObjectType.ShortArray: return new JniShortArray(environment, handle, lifecycle);
                case JniConcreteObjectType.ObjectArray: return new JniObjectArray(environment, handle, lifecycle, -1, JniType.Null);
                default: throw new JniException("TODO");
            }
        }

        internal static JniObject CreateReturnObject(JniEnvironment environment, IntPtr handle, JniMember member)
        {
            if (handle == IntPtr.Zero)
            {
                return null;
            }
            if (member.ReturnArrayRank > 0)
            {
                return JniObjectArray.NDimensionalArrayFromHandle(environment, handle, member.ReturnArrayRank, member.ReturnJniType);
            }
            else
            {
                switch (member.ReturnJniType)
                {
                    case JniType.String: return new JniString(environment, handle, JniObjectLifecycle.Local);
                    case JniType.Throwable: return new JniThrowable(environment, handle, JniObjectLifecycle.Local);
                    case JniType.Class: return new JniClass(environment, handle, JniObjectLifecycle.Local);
                    case JniType.NonPrimitive: return new JniObject(environment, handle, JniObjectLifecycle.Local);
                    default: throw new JniException("TODO");
                }
            }
        }

        public override int GetHashCode()
        {
            return mHandle.GetHashCode();
        }

        public IntPtr Handle
        {
            get { return mHandle; }
        }

        public bool IsDead
        {
            get { return (mLifeCycle == JniObjectLifecycle.Dead); }
        }

        protected static void VerifyEnvironment(JniEnvironment environment)
        {
            VerifyInputObject(environment);
        }

        internal static void VerifyInputObject(object input)
        {
            if (input == null)
            {
                throw new JniException(JniResourceManager.Instance.GetString("invalidObject"));
            }
        }

        internal static void VerifyInputJniObject(JniObject jniObject)
        {
            if (!(jniObject != null && (jniObject.Handle != IntPtr.Zero)))
            {
                throw new JniException("TODO");
            }
        }

        private void VerifyAlive()
        {
            if (this.IsDead)
            {
                throw new JniException("Dead JniObject.");
            }
        }

        public JniObject NewGlobalAutoRelease(JniEnvironment environment)
        {
            return MakeNew(environment, JniObjectLifecycle.GlobalAutoRelease, false);
        }

        public JniObject NewGlobalAutoReleaseAndReleaseThis(JniEnvironment environment)
        {
            return MakeNew(environment, JniObjectLifecycle.GlobalAutoRelease, true);
        }

        public JniObject NewGlobal(JniEnvironment environment)
        {
            return MakeNew(environment, JniObjectLifecycle.Global, false);
        }

        public JniObject NewGlobalAndReleaseThis(JniEnvironment environment)
        {
            return MakeNew(environment, JniObjectLifecycle.Global, true);
        }

        public JniObject NewLocal(JniEnvironment environment)
        {
            return MakeNew(environment, JniObjectLifecycle.Local, false);
        }

        public JniObject NewLocalAndReleaseThis(JniEnvironment environment)
        {
            return MakeNew(environment, JniObjectLifecycle.Local, true);
        }

        private JniObject MakeNew(JniEnvironment environment, JniObjectLifecycle target, bool releaseThis)
        {
            VerifyAlive();
            VerifyEnvironment(environment);
            IntPtr result = IntPtr.Zero;
            if (target == JniObjectLifecycle.Global || target == JniObjectLifecycle.GlobalAutoRelease)
            {
                result = JniEnvironment.Functions.NewGlobalRef(environment.Handle, this.Handle);
            }
            else if (target == JniObjectLifecycle.Local)
            {
                result = JniEnvironment.Functions.NewLocalRef(environment.Handle, this.Handle);
            }
            if (result == IntPtr.Zero)
            {
                JniThrowable.CheckException(environment);
            }
            // TODO - create correct type
            JniObject newObject = JniObject.CreateObjectOfType(environment, result, this, target);
            if (releaseThis)
            {
                this.Release(environment);
            }
            return newObject;
        }

        public void Release(JniEnvironment environment)
        {
            if (this.IsDead)
            {
                return;
            }
            VerifyEnvironment(environment);
            if (this.mLifeCycle == JniObjectLifecycle.Global || this.mLifeCycle == JniObjectLifecycle.GlobalAutoRelease)
            {
                JniEnvironment.Functions.DeleteGlobalRef(environment.Handle, this.Handle);
                // EXCEPTION CHECK: Do nothing, this method safe to call while exception is pending, does
                // not raise additional exceptions.
            }
            else if (this.mLifeCycle == JniObjectLifecycle.Local || this.mLifeCycle == JniObjectLifecycle.LocalJ2N)
            {
                if (this.mLifeCycle == JniObjectLifecycle.Local)
                {
                    environment.RemoveFromCurrentFrame(this);
                }
                JniEnvironment.Functions.DeleteLocalRef(environment.Handle, this.Handle);
                // EXCEPTION CHECK: Do nothing, this method safe to call while exception is pending, does
                // not raise additional exceptions.
            }
            MakeDead();
        }

        internal void PopRelease()
        {
            MakeDead();
        }

        private void MakeDead()
        {
            this.mLifeCycle = JniObjectLifecycle.Dead;
            this.mHandle = IntPtr.Zero;
            if (mWillFinalize)
            {
                GC.SuppressFinalize(this);
                mWillFinalize = false;
            }
        }

        public bool IsSameObject(JniEnvironment environment, JniObject otherObject)
        {
            if (otherObject == null)
            {
                return false;
            }
            if (this.IsDead && otherObject.IsDead)
            {
                return true;
            }
            if ((this.IsDead && !otherObject.IsDead) || (!this.IsDead && otherObject.IsDead))
            {
                return false;
            }
            VerifyEnvironment(environment);
            byte result = JniEnvironment.Functions.IsSameObject(environment.Handle, this.Handle, otherObject.Handle);
            JniThrowable.CheckException(environment);
            return JBooleanConverter.From(result);
        }

        public JniClass GetObjectClass(
            JniEnvironment environment)
        {
            VerifyEnvironment(environment);
            IntPtr result = JniEnvironment.Functions.GetObjectClass(environment.Handle, this.Handle);
            JniThrowable.CheckException(environment);
            return new JniClass(environment, result, JniObjectLifecycle.Local);
        }

        // OK to suppress, match JNI name & signature
        [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")]
        public bool IsInstanceOf(
            JniEnvironment environment,
            JniClass jniClass
            )
        {
            VerifyEnvironment(environment);
            VerifyInputJniObject(jniClass);
            byte result = JniEnvironment.Functions.IsInstanceOf(environment.Handle, this.Handle, jniClass.Handle);
            JniThrowable.CheckException(environment);
            return JBooleanConverter.From(result);
        }

        public string ExtractStringValue(JniEnvironment environment)
        {
            VerifyEnvironment(environment);
            return JniString.ExtractStringFromHandle(environment, this.Handle);
        }
    }
}
